# Makefile for the uv managed uv.lock file

# This variable can be overridden on the make command line
#     make PYTHON=/opt/python-3.12.9/bin/python3.12 venv
PYTHON = python3
PYTHON:=$(shell which $(PYTHON))

MAKEFILE_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
ifneq (${MAKEFILE_DIR}, $(shell pwd))
 $(error You must invoke 'make' from within ${MAKEFILE_DIR})
endif

PYPROJECT_TOML = pyproject.toml
GIT_DIR = .git
PROJECT_NAME:=$(shell basename "$(realpath .)")
GIT_REMOTE = origin

ifeq (,$(wildcard $(PYPROJECT_TOML)))
  $(error "$(PYPROJECT_TOML)" does not exist.)
endif

VERSION := `grep -m1 '^version' $(PYPROJECT_TOML) | sed -E 's/version = "(.*)"/\1/'`
PKG_NAME := $(shell grep -m1 '^name' $(PYPROJECT_TOML) | sed -E 's/name = "(.*)"/\1/')

# Match PEP 440 version forms accepted by this project (used by set-version and tag):
#   N.N.N            — final release
#   N.N.N{a|b|rc}N   — pre-release (a=alpha, b=beta, rc=release candidate)
#   N.N.N.postN      — post-release
#   N.N.N.devN       — development release
PEP440_VERSION = ([0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+|\.post[0-9]+|\.dev[0-9]+)?)

# Local PyPI Repository Configuration
#    Setting LOCAL_PIPY_REPO will enable a local PyPI repository
#    If this local repository requires authentication, set NETRC to .netrc
#LOCAL_PYPI_REPO = https://example.com/api/v4/projects/43/packages/pypi/simple
#NETRC = .netrc

# Security Note:  We search the local repository first with --index-url
# before PyPI with --extra-index-url.  Note that the highest numbered
# version of a package that matches the requirement will be pulled from either
# repository regardless of search order.  However, when both repositories
# have the same highest match, the --index-url repo will be used.  Therefore,
# we must use pinned versions for packages in local repositories.  Otherwise,
# a bad actor could upload a same-named library to PyPI with a higher matching
# version number which would get used in place of our local package.
ifdef LOCAL_PYPI_REPO
  INDEX_URLS = --index-url=$(LOCAL_PYPI_REPO) --extra-index-url=https://pypi.org/simple
else
  INDEX_URLS =
endif

PREFIX =
ifdef NETRC
  ifeq (,$(wildcard $(NETRC)))
    $(error NETRC defined as "$(NETRC)" but file does not exist.)
  endif
  PREFIX += NETRC=$(NETRC)
endif
PREFIX += UV_PROJECT_ENVIRONMENT=venv

UV = $(PREFIX) $(PYTHON) -m uv
UV_RUN          = $(UV) run
UV_LOCK         = $(UV) lock
UV_SYNC_PROD    = $(UV) sync --frozen --no-editable
UV_SYNC_DEVCI   = $(UV) sync --frozen --group devci
UV_BUILD        = $(UV) build --verbose
UV_LOCK_UPGRADE = $(UV) lock --upgrade --verbose

.PHONY: build clean clean-build clean-docs clean-pyc clean-test
.PHONY: coverage docs-build docs-serve ensure-in-venv
.PHONY: ensure-not-in-venv init publish qa set-version sync-devci sync-prod
.PHONY: tag update uv.lock venv version

all: init uv.lock

# Print the current version of the project
version:
	@echo "Current version is $(VERSION)"

ensure-in-venv:
	@$(PYTHON) -c "import sys; sys.exit(sys.prefix == sys.base_prefix)" || \
	{ echo "Error: this target requires $(PYTHON) to be in a venv directory"; exit 1; }

ensure-not-in-venv:
	@$(PYTHON) -c "import sys; sys.exit(sys.prefix != sys.base_prefix)" || \
	{ echo "Error: this target requires $(PYTHON) not to be in a venv directory"; exit 1; }

venv: ensure-not-in-venv
	$(PYTHON) -m venv --clear --upgrade-deps --prompt $(PROJECT_NAME) venv

init: ensure-in-venv
	# Can't use uv since it's not yet installed in the venv we are creating
	$(PYTHON) -m pip install --upgrade pip wheel setuptools uv pre-commit
	if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then $(PYTHON) -m pre_commit install; fi

uv.lock: $(PYPROJECT_TOML)
	rm -f $@
	$(UV_LOCK)

# Install production dependencies only (non-editable); useful for pre-publish verification
sync-prod:
	$(UV_SYNC_PROD)

# Install all development and CI dependencies
sync-devci:
	$(UV_SYNC_DEVCI)

# Run all the formatting, linting, type checking, and testing commands
qa: ensure-in-venv
	if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then $(PYTHON) -m pre_commit run --all-files; fi
	$(PYTHON) -m ruff check --config pyproject.toml
	$(PYTHON) -m mypy --strict --config pyproject.toml
	$(PYTHON) -m ty check
	$(PYTHON) -m pytest

# Run tests with coverage
coverage:
	$(UV_RUN) coverage run -m pytest
	$(UV_RUN) coverage combine
	$(UV_RUN) coverage report
	$(UV_RUN) coverage html

# Serve docs locally with live reload
docs-serve:
	-lsof -ti :8000 | xargs kill
	$(UV_RUN) --isolated --group docs zensical serve

# Build docs (strict mode, fails on warnings)
docs-build:
	$(UV_RUN) --isolated --group docs zensical build --clean

# Build the project, useful for checking that packaging is correct
build:
	rm -rf build
	rm -rf dist
	$(UV_BUILD)

set-version: ensure-in-venv
	@HIST_VER=$$(grep -m1 '^## ' HISTORY.md | sed -E 's/^## $(PEP440_VERSION).*/\1/'); \
	if [ -z "$$HIST_VER" ]; then echo "Error: HISTORY.md top entry is not a release version (still 'Next Release'?)"; exit 1; fi; \
	HIST_DATE=$$(grep -m1 '^## ' HISTORY.md | sed -E 's/^## [^ ]+ \((.*)\)$$/\1/'); \
	if [ "$$HIST_DATE" = "TBD" ]; then echo "Error: HISTORY.md top entry date is still TBD"; exit 1; fi; \
	if git rev-parse "v$$HIST_VER" > /dev/null 2>&1; then echo "Error: tag v$$HIST_VER already exists"; exit 1; fi; \
	echo "Setting version to $$HIST_VER"; \
	$(UV) version --no-sync $$HIST_VER; \
	echo ""; \
	echo "pyproject.toml and uv.lock updated.  Next: git commit -am 'Release $$HIST_VER' && make tag"

tag:
	@HIST_VER=$$(grep -m1 '^## ' HISTORY.md | sed -E 's/^## $(PEP440_VERSION).*/\1/'); \
	TOML_VER=$$(grep -m1 '^version' $(PYPROJECT_TOML) | sed -E 's/version = "(.*)"/\1/'); \
	LOCK_VER=$$(grep -A1 "^name = \"$(PKG_NAME)\"" uv.lock | grep '^version' | sed -E 's/version = "(.*)"/\1/'); \
	if [ -z "$$HIST_VER" ]; then echo "Error: HISTORY.md top entry is not a release version"; exit 1; fi; \
	if [ "$$TOML_VER" != "$$HIST_VER" ]; then echo "Error: pyproject.toml ($$TOML_VER) != HISTORY.md ($$HIST_VER) -- run make set-version first"; exit 1; fi; \
	if [ "$$LOCK_VER" != "$$HIST_VER" ]; then echo "Error: uv.lock ($$LOCK_VER) != HISTORY.md ($$HIST_VER) -- run make set-version first"; exit 1; fi; \
	if git rev-parse "v$$HIST_VER" > /dev/null 2>&1; then echo "Error: tag v$$HIST_VER already exists"; exit 1; fi; \
	echo "Tagging version v$$HIST_VER"; \
	git tag -a v$$HIST_VER -m "Create version v$$HIST_VER"; \
	git push $(GIT_REMOTE) v$$HIST_VER && \
	$(PYTHON) -c "f='HISTORY.md'; s=open(f).read(); open(f,'w').write(s.replace('# History\n\n', '# History\n\n## Next Release (TBD)\n\n* (describe changes here)\n\n', 1))" && \
	echo "" && \
	echo "HISTORY.md updated with a Next Release (TBD) entry.  Commit with: git commit -am 'Start next development cycle'"

update: init
	$(UV_LOCK_UPGRADE)
	if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then $(PYTHON) -m pre_commit autoupdate; fi

# Publish to Package Repository (e.g., PyPI) -- manual alternative to GitLab CI
publish: build
	$(PYTHON) -m pip install twine
	$(UV_PUBLISH)

# remove all build, test, coverage and Python artifacts
clean:
	clean-build
	clean-docs
	clean-pyc
	clean-test

# remove build artifacts
clean-build:
	rm -rf build
	rm -rf dist
	rm -rf .eggs
	find . -name '*.egg-info' -exec rm -rf {} +
	find . -name '*.egg' -exec rm -f {} +

# remove built docs
clean-docs:
	rm -rf site/

# remove Python file artifacts
clean-pyc:
	find . -name '*.pyc' -exec rm -f {} +
	find . -name '*.pyo' -exec rm -f {} +
	find . -name '*~' -exec rm -f {} +
	find . -name '__pycache__' -exec rm -fr {} +

# remove test and coverage artifacts
clean-test:
	rm -f .coverage
	rm -f .coverage.*
	rm -rf htmlcov
	rm -rf .pytest_cache
